import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

"""
手写数字识别（MNIST）
"""

# 设置装备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"device: {device}")

# 超参数
batch_size = 64
learning_rate = 0.001
num_epochs = 10

# 1 加载和预处理数据
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.1307,), (0.3081,))]
                               ) # MNIST数据集的均值和标准差
# 下载训练数据集
train_dataset = torchvision.datasets.MNIST(
    root='./data',
    train=True,
    transform=transform,
    download=True)
# 下载测试数据集
test_dataset = torchvision.datasets.MNIST(
    root='./data',
    train=False,
    transform=transform
)
# 创建数据加载器
train_loader = torch.utils.data.DataLoader(
    dataset=train_dataset,
    batch_size=batch_size,
    shuffle=True
)

test_loader = torch.utils.data.DataLoader(
    dataset=test_dataset,
    batch_size=batch_size,
    shuffle=False
)

# 2. 定义神经网络模型
class NeuralNet(nn.Module):
    def __init__(self):
        super(NeuralNet, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28 * 28, 512),
            nn.ReLU(),
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10)
        )

    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

model = NeuralNet().to(device)
print(f"模型结构：{model}")

# 3. 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)

# 4. 训练模型
total_step = len(train_loader)
losses = []
accuracies = []

for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):
        # 将张量移动到设备
        images = images.to(device)
        labels = labels.to(device)

        # 前向传播
        outputs = model(images)
        loss = criterion(outputs, labels)

        # 反向传播和优化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # 记录损失
        if (i + 1) % 100 == 0:
            losses.append(loss.item())
            print(f'Epoch [{epoch + 1}/{num_epochs}], Step [{i + 1}/{total_step}], Loss: {loss.item():.4f}')

# 5. 测试模型
model.eval()  # 设置模型为评估模式
with torch.no_grad():
    correct = 0
    total = 0
    for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

    accuracy = 100 * correct / total
    print(f'测试准确率: {accuracy:.2f}%')


# 6. 可视化一些预测结果
def imshow(img):
    img = img / 2 + 0.5  # 反标准化
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()


# 获取一些测试样本
dataiter = iter(test_loader)
images, labels = next(dataiter)

# 显示图像
imshow(torchvision.utils.make_grid(images[:4]))

# 打印预测结果
outputs = model(images.to(device))
_, predicted = torch.max(outputs, 1)

print('真实标签:', ' '.join(f'{labels[j]}' for j in range(4)))
print('预测结果:', ' '.join(f'{predicted[j]}' for j in range(4)))

# 7. 保存模型
torch.save(model.state_dict(), 'mnist_model.pth')
print("模型已保存为 'mnist_model.pth'")